<?php

/**
 * Loads the module bootstrap, initializes the module, and registers module-
 * related action and view helpers
 *
 * @author    Mon Zafra <monzee at gmail>
 * @copyright (c)2009 Mon Zafra
 * @category  Mz
 * @package   Module
 * @license   http://mz-project.googlecode.com/svn/trunk/LICENSE     MIT License
 * @version   SVN: $Id: Initializer.php 5 2009-02-19 05:52:22Z monzee $
 */
class Mz_Module_Initializer extends Zend_Controller_Plugin_Abstract
{
    protected $_moduleBootstrapSpec = ':module_Bootstrap';
    protected $_moduleBootstrapFileSpec = ':path/Bootstrap.php';
    protected $_modules = array();
    static protected $_preRouteHooks = array();
    static protected $_autoDiscoverPath;

    /**
     * Constructor.
     *
     * @param array|Zend_Config $opts Options
     * @todo Use Mz_Configure
     */
    public function __construct($opts = array())
    {
        if ($opts instanceof Zend_Config) {
            $opts = $opts->toArray();
        }

        if (!empty($opts)) {
            $this->setOptions($opts);
        }

        // autodiscover modules with preroutes
        if (!empty(self::$_autoDiscoverPath)) {
            $this->autoDiscover();
        }

        // register module public resource mapper
        $front = Zend_Controller_Front::getInstance();

        $resPlugin = new Mz_Module_Resource();
        $resModule = $resPlugin->getResourcesModule();
        $resModulePath = realpath(dirname(__FILE__) . '/Resource/controllers');

        $front->registerPlugin($resPlugin);

        // make sure the $resModule module name is unique
        $modules = array_keys($front->getControllerDirectory());

        $test = $resModule;
        while (in_array($test, $modules)) {
            $test = '_' . $test;
        }
        if ($test != $resModule) {
            $resPlugin->setResourcesModule($test);
        }

        $front->addControllerDirectory($resModulePath, $test);

        // call registered preroutes
        if (!empty(self::$_preRouteHooks)) {
            $this->preRoute();
        }
    }

    /**
     * Sets options from array. Watch the case!
     *
     * @var array $options
     */
    public function setOptions(array $options)
    {
        foreach ($options as $option => $value) {
            $setter = 'set' . ucfirst($option);
            if (is_callable(array($this, $setter))) {
                $this->$setter($value);
            } else if (property_exists($this, '_' . $option) && $option != 'modules') {
                $prop = '_' . $option;
                $this->$prop = $value;
            }
        }
    }

    /**
     * Calls preRoute hooks
     */
    public function preRoute()
    {
        $hooks = self::$_preRouteHooks;
        foreach ($hooks as $module => $moduleDir) {
            $bootstrap = $this->getModuleBootstrapFile($module, $moduleDir);
            include_once $bootstrap;

            $module = $this->getModuleBootstrapClass($module);
            call_user_func_array(array($module, 'preRoute'), array($module, $moduleDir));
            call_user_func(array($module, 'setPreRouted'));
        }
    }

    /**
     * Add many preRoute hooks at once.
     *
     * @param array $modules Names of the modules
     * @todo Should this reset the current values?
     */
    static public function setPreRouteHooks($modules)
    {
        $modules = (array) $modules;
        foreach ($modules as $module) {
            self::addPreRouteHook($module);
        }
    }

    /**
     * Adds a preroute hook.
     *
     * @param string $module Module whose preRoute() must be called
     */
    static public function addPreRouteHook($module, $path = null)
    {
        if (null === $path) {
            $front = Zend_Controller_Front::getInstance();
            $path = $front->getModuleDirectory($module);
        }
        self::$_preRouteHooks[$module] = $path;
    }

    /**
     * Sets the autoDiscover path
     *
     * @param string $path Directory containing the modules
     */
    static public function setAutoDiscover($path)
    {
        self::$_autoDiscoverPath = $path;
    }

    /**
     * Registers modules with defined preRoute() methods.
     *
     * Iterates through the module directory, reads the bootstrap and determines
     * if the preRoute() method is overridden.
     *
     * This is NOT the recommended way of registering modules. You should
     * call addPreRouteHook() for each module or setPreRouteHooks() with an
     * array of modules.
     */
    public function autoDiscover()
    {
        $path = self::$_autoDiscoverPath;
        if (empty($path)) {
            return;
        }

        $front = Zend_Controller_Front::getInstance();
        if (is_string($path)) {
            $front->addModuleDirectory($path);
        }

        $moduleDirs = $front->getControllerDirectory();
        $defaultModule = $front->getDefaultModule();

        foreach ($moduleDirs as $module => $dir) {
            $dir = realpath($dir . '/..');
            $bootstrapFile = $this->getModuleBootstrapFile($module, $dir);
            $bootstrapClass = $this->getModuleBootstrapClass($module);

            if ($module == $defaultModule || !file_exists($bootstrapFile)) {
                continue;
            }

            include_once $bootstrapFile;
            $hook = new ReflectionMethod($bootstrapClass, 'preRoute');
            $declarer = $hook->getDeclaringClass();
            if ($declarer->name === $bootstrapClass) {
                self::addPreRouteHook($module, $dir);
            }
        }
    }

    /**
     * Tries to load the bootstrap of the module matched by the route (if not
     * default).
     *
     * Also makes available all module-related action/view helpers.
     *
     * @param Zend_Controller_Request_Abstract $request
     * @return void
     */
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        Zend_Controller_Action_HelperBroker::addPrefix('Mz_Module_Helper');

        $vr = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');
        $vr->initView();

        $view = $vr->view;
        $view->addHelperPath('Mz/Module/Helper', 'Mz_Module_Helper');

        $front = Zend_Controller_Front::getInstance();

        $defaultModule = $front->getDefaultModule();
        $module = $request->getModuleName();
        if ($module == $defaultModule ) {
            return;
        }

        $this->initModule($module);
    }

    /**
     * Generates the name of the module bootstrap file according to spec.
     *
     * @param string $module
     * @param string $modulePath
     * @return string
     */
    public function getModuleBootstrapFile($module, $modulePath)
    {
        $spec = $this->_moduleBootstrapFileSpec;
        $map = array(':module' => $module, ':path' => $modulePath);
        return str_replace(array_keys($map), array_values($map), $spec);
    }

    /**
     * Generates the module bootstrap class name according to spec.
     *
     * @param string $module
     * @return string
     */
    public function getModuleBootstrapClass($module)
    {
        $spec = $this->_moduleBootstrapSpec;
        $module = ucfirst($module);
        return str_replace(':module', $module, $spec);
    }

    /**
     * Initializes the module using a static method of the module bootstrap.
     *
     * @param string $module
     * @return bool Was module initialized?
     */
    public function initModule($module)
    {
        // return early if already initialized
        if (isset($this->_modules[$module])) {
            return true;
        }

        // return false if $module is not a valid module
        $modulePath = $this->getModulePath($module);
        if (!$modulePath) {
            return false;
        }

        $bootstrap = $this->getModuleBootstrapFile($module, $modulePath);

        $initialized = false;
        if (file_exists($bootstrap)) {
            $moduleClass = $this->getModuleBootstrapClass($module);

            include_once $bootstrap;
            $moduleObj = new $moduleClass($module, $modulePath);
            
            $this->_modules[$module] = $moduleObj;
            $initialized = true;
        }

        return $initialized;
    }

    /**
     * Returns an instance of module bootstrap.
     *
     * @param string $module Module name
     * @return Mz_Module_Bootstrap False if not found
     */
    public function getModule($module)
    {
        $ret = $this->initModule($module);
        if (false === $ret) {
            return false;
        }
        return $this->_modules[$module];
    }

    /**
     * Gets the module directory.
     *
     * @param string $module Module name
     * @return string Path to module, false if none found
     */
    public function getModulePath($module)
    {
        $front = Zend_Controller_Front::getInstance();
        $modulePath = $front->getModuleDirectory($module);
        if (null === $modulePath) {
            $modulePath = false;
        }

        return $modulePath;
    }

    /**
     * Registers self to the front controller plugin stack.
     *
     * See the notes on autoDiscover()
     * 
     * @param mixed $opts Array of options for the constructor or string
     *                    containing module dir to autodiscover or true to
     *                    autodiscover from currently defined controller dirs
     */
    static public function startMvc($opts = null)
    {
        if (is_string($opts) || true === $opts) {
            self::$_autoDiscoverPath = $opts;
            $opts = array();
        }

        $front = Zend_Controller_Front::getInstance();
        if (!$front->hasPlugin(__CLASS__)) {
            $front->registerPlugin(new self($opts));
        }
    }

}
